package IC.Visitors;

import java.util.List;

import IC.AST.*;
import IC.SymbolTable.*;
import IC.TypeTable.TypeTable;

public class buildingSymbolTables implements Visitor 
{
	private String icFileName;
	private boolean hasMain = false;

	// constructor
	public buildingSymbolTables(String file) throws SemanticError 
	{

		this.icFileName =  file.substring( file.lastIndexOf("\\")+1);

		try {
			TypeTable.initTypeTable(icFileName);
		} 
		catch (SemanticError e) 
		{
			throw new SemanticError(e.getMessage(), 0);

		}
	}

	// ************************************************************************

	private static boolean isMainMethod(MethodSymbol msym) throws SemanticError 
	{
		if (!msym.isStaticMethod()) // method is not static
		{
			return false;
		}

		if (msym.getId().compareTo("main") != 0) // method's name isn't "main"
		{
			return false;
		}
		IC.TypeTable.MethodType mtype = (IC.TypeTable.MethodType) msym
				.getType();

		if (!mtype.getReturnType().isSubTypeOf(TypeTable.getType("void"))) 
			// return type is not void
		{
			return false;
		}
		List<IC.TypeTable.Type> l = mtype.getListTypes();
		if (l.size() != 1) // no parameters or too many parameters
		{	return false;
		}
		IC.TypeTable.Type t = l.get(0);

		if (!t.isSubTypeOf(TypeTable.addArrayType(TypeTable
				.getType("string")))) 
		{
			// param is not of type string[]
			return false;
		}

		return true;
	}

	// ************************************************************************
	public Object visit(Program p) throws SemanticError 
	{

		GlobalSymbolTable global = new GlobalSymbolTable(this.icFileName);
		
		for (ICClass c : p.getClasses()) 
		{
			try 
			{
				
				global.addClass(c);
			} 
			catch (SemanticError se) 
			{
				throw new SemanticError(se.getMessage());
			}
		}
		for (ICClass c : p.getClasses()) 
		{
			c.setEnclosingScope(global);
			ClassSymbolTable cst = (ClassSymbolTable) c.accept(this);
			if (cst == null)
				return null;
			else if (c.hasSuperClass()) 
			{
				
				// if c has superclass, we add cst to the children list of the
				// SymbolTable of his superclass
				ClassSymbolTable cst2 = global.lookupClassSymbolTableRec(c.getSuperClassName());
				
				if(cst2!=null)
				{
					
					cst2.addChildTable(cst.getId(), cst);

				}
			} 
			else 
			{
				// if c doesn't have a superclass, we add cst to the children
				// list of the global symbol table
				
				global.addChildTable(cst.getId(), cst);
				
			}

		}
		if (!this.hasMain) 
		{


			throw new SemanticError("Program has no main method",	p.getLine());

		}
		return global;
	}

	// ************************************************************************

	public Object visit(ICClass icc) throws SemanticError 
	{
		
		ClassSymbolTable cst;
		GlobalSymbolTable global = (GlobalSymbolTable) icc.getEnclosingScope();
		if (icc.hasSuperClass()) 
		{
			cst = new ClassSymbolTable(icc.getName(), global
					.lookupClassSymbolTableRec(icc.getSuperClassName())
					, global);
		} else
		{
			cst = new ClassSymbolTable(icc.getName(), global);
		}

		// recursively fill class symbol table



		// fields:
		// ---------
		for (Field f : icc.getFields()) 
		{
			f.setEnclosingScope(cst);


			if (cst.isDeclaredInThisOrSuperClass(f.getName(), 'b'))
			{
				// if got till here, field is
				// previously defined as field or method (in this class or in
				// superclass), print error
				throw new SemanticError("field '" + f.getName() + "' was previously defined",f.getLine());
			}



			// field was not previously defined

			try 
			{
				cst.addFieldSymbol(f.getName(), f.getType().getArrName(),f.getLine());


			} 
			catch (SemanticError se2) // the field's type is undefined
			{
				throw new SemanticError(se2.getMessage(), f.getLine());

			}

			// recursively call visitor
			if (f.accept(this) == null)
				return null;

		}

		

		// methods:
		// ---------

		for (Method m : icc.getMethods()) 
		{
			m.setEnclosingScope(cst);
			MethodSymbol msym;

			try {
				msym = new MethodSymbol(m);
				// check if this method is "main" and check uniqueness
				if (isMainMethod(msym)) 
				{
					if (this.hasMain) 
					{
						// already have "main" method, throw error

						throw new SemanticError("Program already has main method");
					} else // this is first occurrence of main method
					{
						this.hasMain = true;
					}
				}
			} 
			catch (SemanticError se) 
			{
				// semantic error while creating the method symbol (some
				// semantic type error)
				throw new SemanticError(se.getMessage(), m.getLine());
			}

			// check if m was previously defined as field or method
			// if method was previously defined in this class scope or in a super
			// class with a different signature (including if it's static or not), error.
			// else, add a new MethodSymbol to the Symbol Table.

			if (cst.isDeclaredInThisOrSuperClass(m.getName(), 'f'))
			{
				// if got here, method was previously defined as field, print
				// error

				throw new SemanticError("method " + m.getName() + " was previously defined as field", m.getLine());
			}

			// if got here, method wasn't previously defined as field

			if (cst.isDeclaredInThisClass(m.getName(), 'm')) 
			{

				// if got here, method was previously defined in this class,
				// print error


				throw new SemanticError("method " + m.getName()	+ 
						" previously defined in this class"	, m.getLine());
			}

			MethodSymbol prevDefMethod = null;

			prevDefMethod = cst.lookupMethodSymbol(m.getName());



			if	(prevDefMethod != null)
			{	


				if (!prevDefMethod.getType().equals(msym.getType())
						|| (prevDefMethod.isStaticMethod() != msym.isStaticMethod())) 
				{

					// if got here, method was previously defined in
					// super-class with a different signature, print error

					throw new SemanticError("method "+ m.getName()+ " was previously defined in super class. " +
							"Overloading not allowed",
							m.getLine());
				} 
				else // overriding method -
					//(method was previously defined in super-class with the same signature
				{
					cst.addMethodSymbol(m.getName(), msym);
				}

			}

			else
			{
				// method wasn't previously defined
				cst.addMethodSymbol(m.getName(), msym);
			}

		}

		// recursive method symbol table build
		for (Method m : icc.getMethods()) 
		{
			MethodSymbolTable mst = (MethodSymbolTable) m.accept(this);
			if (mst == null) 
			{
				return null;
			} else
				cst.addChildTable(mst.getId(), mst);
		}

		return cst;
	}

	// ************************************************************************

	public MethodSymbolTable methodVisit(Method method) throws SemanticError 
	{
		
		MethodSymbolTable mst;

		mst = new MethodSymbolTable(method.getName(),
				(ClassSymbolTable) method.getEnclosingScope());

		// add return type symbol

		// set enclosing scope for return type


		method.getType().setEnclosingScope(mst);

		try 
		{
			mst.setReturnParamSymbol(method.getType().getArrName(),method.getLine());
		} 
		catch (SemanticError e2) 
		{
			throw new SemanticError(e2.getMessage(), method.getLine());
		}

		for (Formal f : method.getFormals()) 
		{
			f.setEnclosingScope(mst);


			if(	mst.isDeclaredAlreadyInCurrScopeMethod(f.getName()))
			{
				// if got here, parameter was previously defined in this method
				throw new SemanticError("parameter " + f.getName()+ " was previously defined in method "
						+ method.getName(), f.getLine());
			}


			// if got here, parameter is undefined,
			// insert its symbol to this method's symbol table

			try 
			{
				mst.addformalParamSymbol(f.getName(), f.getType().getArrName(),f.getLine());

			} 
			catch (SemanticError e1) 
			{
				// semantic error while creating the parameter type symbol
				throw new SemanticError(e1.getMessage(), f.getLine());
			}

			if (f.accept(this) == null)
				return null;

		}

		// recursive call to visitor
		for (Statement s : method.getStatements()) 
			s.setEnclosingScope(mst);

		for (Statement s : method.getStatements()) 
			if (s.accept(this)==null)return null;



		return mst;
	}

	// ************************************************************************

	public Object visit(StaticMethod method) throws SemanticError 
	{
		return methodVisit(method);
	}

	// ************************************************************************

	public Object visit(VirtualMethod method) throws SemanticError 
	{
		return methodVisit(method);
	}

	// ************************************************************************

	public Object visit(LibraryMethod method) throws SemanticError 
	{
		return methodVisit(method);
	}

	// ************************************************************************

	public Object visit(StatementsBlock statementsBlock) throws SemanticError 
	{
		BlockSymbolTable blockst = new BlockSymbolTable(statementsBlock
				.getEnclosingScope());

		// get this table's parent (block / method symbol table)
		BlockSymbolTable blockst_parent = (BlockSymbolTable) statementsBlock
				.getEnclosingScope();


		// recursive call to visitor
		for (Statement s : statementsBlock.getStatements()) 
		{
			// set enclosing scope
			s.setEnclosingScope(blockst);

			if (s.accept(this) == null)
				return null;
		}

		// add this block symbol table to its father's (method/block symbol
		// table) children list
		blockst_parent.addChildTable(blockst.getId(), blockst);
		return blockst;
	}

	// ************************************************************************

	public Object visit(LocalVariable localVariable) throws SemanticError 
	{	BlockSymbolTable bst = (BlockSymbolTable) localVariable
	.getEnclosingScope();


	if (bst.isDeclaredAlreadyInCurrScopeBlock(localVariable.getName()))
	{
		// if got here, local variable was previously defined in this method block

		throw new SemanticError("variable " + localVariable.getName() + " was previously defined",
				localVariable.getLine());
	}

	// local variable is undefined, insert its symbol to block/method
	// symbol table
	try 
	{
		bst.addVariableSymbol(localVariable.getName(), localVariable.getType().getArrName(),localVariable.getLine());
	} 
	catch (SemanticError se) 
	{
		// semantic error while creating the local variable type symbol
		throw new SemanticError(se.getMessage(),localVariable.getLine());

	}


	// recursive call to visitor
	if (localVariable.hasInitValue()) 
	{
		localVariable.getInitValue().setEnclosingScope(localVariable.getEnclosingScope());
		if (localVariable.getInitValue().accept(this) == null)
			return null;
	}

	localVariable.getType().setEnclosingScope(localVariable.getEnclosingScope());
	if (localVariable.getType().accept(this) == null)
		return null;

	return true;

	}


	// #########################################################################


	public Object visit(Field field) throws SemanticError {
		field.setEnclosingScope(field.getEnclosingScope());
		return field.getType().accept(this);
	}

	public Object visit(Formal formal) throws SemanticError {
		formal.setEnclosingScope(formal.getEnclosingScope());
		if (formal.getType().accept(this)==null)return null;
		return true;
	}


	public Object visit(PrimitiveType type){
		return true;
	}

	public Object visit(UserType type) {
		return true;
	}

	public Object visit(Assignment assignment) throws SemanticError {		

		assignment.getAssignment().setEnclosingScope(assignment.getEnclosingScope());
		if (assignment.getAssignment().accept(this)==null) return null;
		assignment.getVariable().setEnclosingScope(assignment.getEnclosingScope());
		if(assignment.getVariable().accept(this)==null)return null;


		return true;
	}

	public Object visit(CallStatement callStatement) throws SemanticError {
		
		callStatement.getCall().setEnclosingScope(callStatement.getEnclosingScope());
		if (callStatement.getCall().accept(this) == null) return null;

		return true;
	}

	public Object visit(Return returnStatement) throws SemanticError {
		if (returnStatement.hasValue()){
			returnStatement.getValue().setEnclosingScope(returnStatement.getEnclosingScope());
			if (returnStatement.getValue().accept(this) == null) return null;
		}
		return true;
	}


	public Object visit(If ifStatement) throws SemanticError {
		ifStatement.getCondition().setEnclosingScope(ifStatement.getEnclosingScope());

		if (ifStatement.getCondition().accept(this)==null)return null;


		//check if operation has one or more operations
		Statement operation = ifStatement.getOperation();
		if (operation instanceof LocalVariable){
			BlockSymbolTable bst = new BlockSymbolTable(ifStatement.getEnclosingScope());
			((BlockSymbolTable)ifStatement.getEnclosingScope()).addChildTable(bst.getId(),bst);
			operation.setEnclosingScope(bst);     
		} else operation.setEnclosingScope(ifStatement.getEnclosingScope());
		if (operation.accept(this) == null) return null;

		if (ifStatement.hasElse()){
			// in case of a LocalVariable statement, create new BlockSymbolTable
			Statement elseOperation = ifStatement.getElseOperation();
			if (elseOperation instanceof LocalVariable){
				BlockSymbolTable bst = new BlockSymbolTable(ifStatement.getEnclosingScope());
				((BlockSymbolTable)ifStatement.getEnclosingScope()).addChildTable(bst.getId(),bst);
				elseOperation.setEnclosingScope(bst);  
			} else elseOperation.setEnclosingScope(ifStatement.getEnclosingScope());
			if (elseOperation.accept(this) == null) return null;
		}
		return true;

	}

	public Object visit(While whileStatement) throws SemanticError {
		whileStatement.getCondition().setEnclosingScope(whileStatement.getEnclosingScope());
		if (whileStatement.getCondition().accept(this)==null)return null;

		Statement operation = whileStatement.getOperation();
		if (operation instanceof LocalVariable){
			BlockSymbolTable bst = new BlockSymbolTable(whileStatement.getEnclosingScope());
			((BlockSymbolTable)whileStatement.getEnclosingScope()).addChildTable(bst.getId(),bst);
			operation.setEnclosingScope(bst);      
		} else operation.setEnclosingScope(whileStatement.getEnclosingScope());
		if (operation.accept(this) == null) return null;


		return true;
	}

	public Object visit(Break breakStatement) {
		return true;
	}

	public Object visit(Continue continueStatement) {	
		return true;
	}

	public Object visit(VariableLocation location) throws SemanticError {
		if (location.isExternal()){ // field location is not null
			location.getLocation().setEnclosingScope(location.getEnclosingScope());


			if (location.getLocation().accept(this) == null) return null;

		}else 
			/// check that the location is previously defined
		{		
		if (!((BlockSymbolTable) location.getEnclosingScope()).isDeclaired(location.getName())){


			throw new SemanticError("location: '"+location.getName()+"' was not previously defined",location.getLine());
		}
		
		}
		return true;
	}


	public Object visit(ArrayLocation location) throws SemanticError {
		location.getArray().setEnclosingScope(location.getEnclosingScope());
		if (location.getArray().accept(this) == null) return null;

		location.getIndex().setEnclosingScope(location.getEnclosingScope());
		if (location.getIndex().accept(this) == null) return null;

		return true;

	}

	public Object visit(StaticCall call) throws SemanticError {
		for (Expression e: call.getArguments()){
			e.setEnclosingScope(call.getEnclosingScope());
			if (e.accept(this) == null) return null;
		}

		return true;
	}

	public Object visit(VirtualCall call) throws SemanticError {
		if (call.isExternal()) { // field location is not null
			call.getLocation().setEnclosingScope(call.getEnclosingScope());
			if (call.getLocation().accept(this) == null) return null;
		}

		for (Expression e: call.getArguments()){
			e.setEnclosingScope(call.getEnclosingScope());
			if (e.accept(this) == null) return null;
		} 
		return true;
	}

	public Object visit(This thisExpression) {
		return true;
	}

	// WHATS THAT?
	public Object visit(NewClass newClass) {
		return true;
	}

	public Object visit(NewArray newArray) throws SemanticError {
		newArray.getType().setEnclosingScope(newArray.getEnclosingScope());
		if (newArray.getType().accept(this) == null) return null;

		newArray.getSize().setEnclosingScope(newArray.getEnclosingScope());
		if (newArray.getSize().accept(this) == null) return null;

		return true;
	}


	public Object visit(Length length) throws SemanticError {
		length.getArray().setEnclosingScope(length.getEnclosingScope());
		if (length.getArray().accept(this) == null) return null;

		return true;}

	public Object visit(MathBinaryOp binaryOp) throws SemanticError {
		binaryOp.getFirstOperand().setEnclosingScope(binaryOp.getEnclosingScope());
		if (binaryOp.getFirstOperand().accept(this) == null) return null;

		binaryOp.getSecondOperand().setEnclosingScope(binaryOp.getEnclosingScope());
		if (binaryOp.getSecondOperand().accept(this) == null) return null;

		return true;
	}

	public Object visit(LogicalBinaryOp binaryOp) throws SemanticError {
		binaryOp.getFirstOperand().setEnclosingScope(binaryOp.getEnclosingScope());
		if (binaryOp.getFirstOperand().accept(this) == null) return null;

		binaryOp.getSecondOperand().setEnclosingScope(binaryOp.getEnclosingScope());
		if (binaryOp.getSecondOperand().accept(this) == null) return null;

		return true;
	}

	public Object visit(MathUnaryOp unaryOp) throws SemanticError {
		unaryOp.getOperand().setEnclosingScope(unaryOp.getEnclosingScope());
		
		
		if (unaryOp.getOperand().getClass().equals(Literal.class))
		{
			Literal l = (Literal)unaryOp.getOperand(); 
			
			if (l.getType().getValue().equals(IC.DataTypes.INT.getDefaultValue()))
			{
				
				l.setValue("-".concat(l.getValue().toString()));
			}
						
		}
		
		if (unaryOp.getOperand().accept(this) == null) return null;
		
		return true;

	}

	public Object visit(LogicalUnaryOp unaryOp) throws SemanticError {
		unaryOp.getOperand().setEnclosingScope(unaryOp.getEnclosingScope());
		if (unaryOp.getOperand().accept(this) == null) return null;

		return true;
	}

	public Object visit(Literal literal) throws SemanticError {
	
		if (literal.getType().getDescription().compareTo(IC.LiteralTypes.INTEGER.getDescription())==0)
		{
		
		try{
			long num = Long.valueOf(literal.getValue().toString());
		if ((num > 2147483647) || (num < -2147483648))
		{
			throw new SemanticError("Number : "+num+ " is out of bound " , literal.getLine());
		}
		
		if (num <0)
		{
			literal.setValue((int)(-num));
		}
		
		else 
		{
			literal.setValue((int)(num));
		}
		
		}catch(Exception e)
		{
			throw new SemanticError("Number : "+literal.getValue().toString()+ " is out of bound " , literal.getLine());
		}
		
		
		}
		return true;
	}


	public Object visit(ExpressionBlock expressionBlock) throws SemanticError {
		expressionBlock.getExpression().setEnclosingScope(expressionBlock.getEnclosingScope());
		if (expressionBlock.getExpression().accept(this) == null) return null;

		return true;
	}

}
